package tool.analyzer
 
trait Analyzer {
	self: Reporter =>

	import parser.Trees._
	import Symbols._
    import scala.collection.mutable._
	import tool.analyzer.Types._
	import tool.analyzer.InitialSymbols._

	var varUseMap: HashSet[Symbol] = new HashSet[Symbol]()
    var varDeclMap: HashSet[Symbol] = new HashSet[Symbol]()

	def analyzeSymbols(prog: Program): GlobalScope = {
		val gs = collectSymbols(prog)
		terminateIfErrors
		setSymbols(prog, gs)
		gs
	}

	private def collectSymbols(prog: Program): GlobalScope = {
		var gs = new GlobalScope
		gs.mainClass = collectObjectSymbol(prog.main)
		gs.classes = InitialSymbols.classes

		prog.classes.foreach(g => {
			val s: ClassSymbol = collectClassSymbol(g,gs)
			if(gs.classes.contains(s.name)) { 
			    var pos:Int=0;
			    g match {
			       case ClassDecl(id,_,_) => pos = id.pos
		           case ClassWithExtends(id,_,_,_) => pos = id.pos
			    }
				error("Class with same identifier already exists", pos)
			}else if(s.name == gs.mainClass.name){
				error("Class cannot have the same identifier as the main object") 
			}else
				gs.classes += new Pair(s.name, s)
		})
		gs
	}

	private def collectObjectSymbol(mainObject: MainObject): ClassSymbol = {
		val s = new ClassSymbol(mainObject.id.value); s.setPos(mainObject.id)
		s.setPos(mainObject)
		mainObject.setSymbol(s) 
		s
	}

	private def collectClassSymbol(c: Class, gs: GlobalScope): ClassSymbol = c match{
		case ClassDecl(id, vars, mets) => val s = getClassScope(id, vars, mets, gs); c.setSymbol(s); s
		case ClassWithExtends(id, sup, vars, mets) =>
			val s = getClassScope(id, vars, mets, gs); c.setSymbol(s); s.parent = Some(new ClassSymbol(sup.value)); s
	}
 
	/**
	 * Collects the variables and the methods 
	 * of a class, checking for duplicate method or variable
	 * definitions.
	 */
	private def getClassScope(id: Identifier, vars: List[VarDecl], mets: List[MethDecl],gs: GlobalScope) : ClassSymbol = {
		var c = new ClassSymbol(id.value); c.setPos(id)
		vars.foreach(g => 
			if(c.members.contains(g.id.value)) // two class variables with the same identifier : name conflict (no member overloading)
				error("Member already declared", g.id.pos)
			else
				c.members += new Pair(g.id.value, collectVarSymbol(g))
		)
		mets.foreach(g => {
			if(c.methods.contains(g.id.value)) // two methods with the same identifier : name conflict (no method overloading)
				error("Two methods with the same identifier", g.id.pos)
			else
				c.methods += new Pair(g.id.value, collectMethodSymbol(g, c, gs))
          }
		)
		c
	}

	private def collectMethodSymbol(met : MethDecl, c : ClassSymbol, gs: GlobalScope) : MethodSymbol = {
		val scope = new MethodSymbol(met.id.value, c); scope.setPos(met.id)
		scope.argList = List[VariableSymbol]()
		met.setSymbol(scope)
		met.args.foreach(g =>
			if(scope.params.contains(g.id.value))
				error("Two parameters with the same name", g.id.pos)
			else{
				val argSym = collectArgumentSymbol(g, scope)
				scope.params += new Pair(g.id.value, argSym)
				scope.argList = scope.argList ::: List(argSym)
			}
		)

		met.varDecls.foreach(g =>
			if(scope.members.contains(g.id.value) || scope.params.contains(g.id.value))
				error("Variable name already used in this method", g.id.pos)
			else
				scope.members += new Pair(g.id.value, collectVarSymbol(g))
		)
  
		scope
	}
    
	private def collectArgumentSymbol(a: Argument, parent: Symbol): VariableSymbol = {
	   	val sym = new VariableSymbol(a.id.value); sym.setPos(a.id)
		varDeclMap += sym
		a.id.setSymbol(sym)
		sym
	}

	private def collectVarSymbol(v: VarDecl): VariableSymbol = {
		val sym = new VariableSymbol(v.id.value); sym.setPos(v.id);
		varDeclMap += sym
		v.id.setSymbol(sym)
		sym
	}

	var currentClassSymbol: ClassSymbol = null
	var currentMethodSymbol: MethodSymbol = null

	private def setSymbols(prog: Program, gs: GlobalScope): Unit = {
		// starts by settings the type symbols on the variable declaration, method declarations and arguments
		// we first need to get all the class symbols prior to call this method because classes represent types and
		// may be used as variable or argument type, as well as return type
		setTypeSymbolsAST(prog, gs) 

		// at this point, every method and variable declaration have a signature (i.e. a name plus a type)
		prog.classes.foreach(g => {
			currentClassSymbol = g.getSymbol
			val sup = checkInheritance(g.getSymbol, gs)
			sup match{
				case Some(p) => g match{case ClassWithExtends(_, supr, _, _) => supr.setSymbol(p) case _ =>}	
				case None => 
			}
		})
		setSymbolsAST(prog, gs)
	}

	private def checkInheritance(cs: ClassSymbol, gs: GlobalScope): Option[ClassSymbol]= {
		var sup: String = ""
		cs.parent match{
			case None => return None
			case Some(superClass) => sup = superClass.name
		}
		var supSymbol = gs.lookupClass(sup)
		supSymbol match{
			case None => 
				if(sup == gs.mainClass.name){
					error("Cannot extend main object", cs.pos)
				}else{
					error("Cannot extend undefined class", cs.pos)
				}
				cs.parent = None
			case Some(p) =>
				if(p == currentClassSymbol){
					error("Cycles in inheritance graph", cs.pos)
					cs.parent = None
				}else{
					p.parent match{
						case Some(p2) => checkInheritance(p, gs)
						case None =>
					}
					checkMethodsOverriding(p)
					checkClassVariables(p)
					cs.parent = supSymbol
				}
		}
		supSymbol
	}

	private def checkMethodsOverriding(cs: ClassSymbol): Unit = {
		currentClassSymbol.methods.values.foreach(g => 
				cs.lookupMethod(g.name) match{
					case Some(p) => 
						if(p.params.size != g.params.size){
							error("Method already defined in a superclass", g.pos)
							error("already defined here", p.pos)
						}else if(p.getType != g.getType){
							error("Methods with same name don't have the same return type -- overriding doesn't apply", g.pos)
						}else{
							checkParams(p.argList, g.argList)
						}
					case None => 
				}
		)
	}

	private def checkParams(l1: List[VariableSymbol], l2: List[VariableSymbol]): Unit = l1 match{
		case x :: xs =>
			if(x.getType != l2.head.getType){
				error("Method signatures are different -- overriding doesn't apply", x.pos)
				error("Type different", l2.head.pos)
			}
			checkParams(xs, l2.tail)
		case Nil =>
	}

	private def checkClassVariables(cs: ClassSymbol): Unit = {
		currentClassSymbol.members.values.foreach(g =>
				cs.lookupVar(g.name) match{
					case Some(p) => error("Field cannot be overriden", g.pos)
					case None =>
				}
		)
	}
	
	private def setSymbolsAST(t: Tree, gs: GlobalScope): Unit = t match{
		case Program(main, classes) => 
			setSymbolsAST(main, gs)
			classes.foreach(g => setSymbolsAST(g, gs))
			varDeclMap.elements.foreach(g => if (!varUseMap.contains(g)) warn("Unused variable " + g.name, g.pos))

		case MainObject(id, stat) => 
			currentClassSymbol = new ClassSymbol("MainObject").setPos(id)
			currentMethodSymbol = new MethodSymbol("MainMethod", currentClassSymbol).setPos(stat)
			setSymbolsAST(stat, gs)

		case cl @ ClassDecl(id, varDecls, metDecls) => 
			currentClassSymbol = cl.getSymbol
			metDecls.foreach(g => setSymbolsAST(g, gs))

		case cl @ ClassWithExtends(id, superClass, varDecls, metDecls) =>
			currentClassSymbol = cl.getSymbol;
			metDecls.foreach(g => setSymbolsAST(g, gs))

		case mt @ MethDecl(id, args, returnType, throws, varDecls, sts, ret) =>
			currentMethodSymbol = mt.getSymbol
			currentMethodSymbol.exceptionThrow = getExceptionSymbols(throws, gs)
			(throws zip currentMethodSymbol.exceptionThrow) foreach { g => g._1 setSymbol g._2 }
			setSymbolsAST(sts, gs)
			setSymbolsAST(ret, gs)

		case While(test, stat) => setSymbolsAST(test, gs); setSymbolsAST(stat, gs)

		/**
		 * Code for exception handling
		 *
		 */
        case TryCatchBlock(t, catches, Some(f)) => setSymbolsAST(TryCatchBlock(t, catches, None), gs); setSymbolsAST(f, gs)                    

		case TryCatchBlock(t, catches, None) => 
			setSymbolsAST(t, gs)
			catches.foreach(g => setSymbolsAST(g, gs))

		// only case where a new symbol is created in the setSymbolsAST method
		case Catch(id, ty, body) =>
			currentMethodSymbol.lookupVar(id.value) match {
			  case Some(v) => error("Identifier already used : " + id.value, id)
              case None =>
                val sym = new ExceptionSymbol(id.value).setPos(id)
				sym.setType(setTypeSymbol(ty, gs))
				id.setSymbol(sym)

				currentMethodSymbol.members += new Pair(id.value, sym)
				setSymbolsAST(body, gs)
				currentMethodSymbol.members -= id.value
            }
		case Throw(expr) => setSymbolsAST(expr, gs)
        case Finally(body) => setSymbolsAST(body, gs)
		// end of exception handling code

		case If(test, stat) => setSymbolsAST(test, gs); setSymbolsAST(stat, gs)
		case IfElse(test, stat, eStat) => setSymbolsAST(test, gs); setSymbolsAST(stat, gs); setSymbolsAST(eStat, gs)
		case Println(expr) => setSymbolsAST(expr, gs)
		case Assign(id, expr) => setSymbolsAST(id, gs); setSymbolsAST(expr, gs)
		case AssignArrayCell(id, idx, value) => setSymbolsAST(id, gs); setSymbolsAST(idx, gs); setSymbolsAST(value, gs)
		case Block(stats) => stats.foreach(g => setSymbolsAST(g, gs))
		case Plus(rhs, lhs) => setSymbolsAST(rhs, gs); setSymbolsAST(lhs, gs)
		case Minus(rhs, lhs) => setSymbolsAST(rhs, gs); setSymbolsAST(lhs, gs)
		case Times(rhs, lhs) => setSymbolsAST(rhs, gs); setSymbolsAST(lhs, gs)
		case Div(rhs, lhs) => setSymbolsAST(rhs, gs); setSymbolsAST(lhs, gs)
		case And(rhs, lhs) => setSymbolsAST(rhs, gs); setSymbolsAST(lhs, gs)
		case Or(rhs, lhs) => setSymbolsAST(rhs, gs); setSymbolsAST(lhs, gs)
		case Equals(rhs, lhs) => setSymbolsAST(rhs, gs); setSymbolsAST(lhs, gs)
		case LessThan(rhs, lhs) => setSymbolsAST(rhs, gs); setSymbolsAST(lhs, gs)
		case Not(v) => setSymbolsAST(v, gs)
		case ArrayCell(tab, idx) => setSymbolsAST(tab, gs); setSymbolsAST(idx, gs)
		case Length(tab) => setSymbolsAST(tab, gs)

		case MethodCall(instance, _, args) => setSymbolsAST(instance, gs); args.foreach(g => setSymbolsAST(g, gs))

		case ArrayCreation(size) => setSymbolsAST(size, gs)
		case Instanciation(cl) => setSymbol(t, gs)
		case id @ Identifier(v) => 
			setSymbol(id, gs)
			if(v != "this") {
				try {
					varUseMap += id.getSymbol
				}catch {case e: java.lang.RuntimeException => Nil}}
		case _ =>
	}

	private def setSymbol(t:Tree, gs: GlobalScope): Unit = t match{
		case i @ Identifier(v) => 
			if(v == "this"){
				i.setSymbol(new VariableSymbol("this").setPos(i))
				i.getSymbol.asInstanceOf[VariableSymbol].setType(TObject(currentClassSymbol))	
			}else
				currentMethodSymbol.lookupVar(v) match{
					case Some(c) => i.setSymbol(c)
					case None => 
						currentClassSymbol.lookupVar(v) match{
							case Some(c) => i.setSymbol(c)
							case None => error("Unknown variable", i.pos)
						}
				}

		case Instanciation(id) => 
			if(id == gs.mainClass.name){
				error("Cannot instantiate main object", id.pos)
			}else{
				gs.lookupClass(id.value) match{
					case None => error("Class not found : " + id.value, t.pos)
					case Some(c) => id.setSymbol(c)
				}
			}
		
		case _ => //so that scalac is happy and doesn't print tons of warnings
	}

	private def getExceptionSymbols(l: List[Identifier], gs: GlobalScope): List[ExceptionSymbol] = {
		def helper(i: Identifier): ExceptionSymbol = {
			gs.lookupClass(i.value) match{
				case None =>
					error("Unknown type : " + i.value, i)
					null
				case Some(p) =>
					(new ExceptionSymbol(i.value)).setType(TObject(p))
			}
		}

		l map {helper _}
	}


	private def setTypeSymbolsAST(t: Tree, gs: GlobalScope): Unit = t match {
		case Program(main, classes) => classes.foreach(g => setTypeSymbolsAST(g, gs))

		case ClassDecl(id, varDecls, metDecls) => 
			varDecls.foreach(g => setTypeSymbolsAST(g, gs))
			metDecls.foreach(g => setTypeSymbolsAST(g, gs))

		case ClassWithExtends(id, sup, varDecls, metDecls) =>
			varDecls.foreach(g => setTypeSymbolsAST(g, gs))
			metDecls.foreach(g => setTypeSymbolsAST(g, gs))

		case VarDecl(id, typ) => 
			val varType = setTypeSymbol(typ, gs)
			id.getSymbol.asInstanceOf[VariableSymbol].setType(varType)

		case Argument(id, typ) =>
			val argTyp = setTypeSymbol(typ, gs)
			id.getSymbol.asInstanceOf[VariableSymbol].setType(argTyp)

		case mt @ MethDecl(id, args, returnType, throws, varDecls, sts, returnExpr) =>
			args.foreach(g => setTypeSymbolsAST(g, gs))
			varDecls.foreach(g => setTypeSymbolsAST(g, gs))
			val retTyp = setTypeSymbol(returnType, gs)
            val mSymbol = mt.getSymbol
			mSymbol.setType(retTyp)  

		case _ => 
	}

	private def setTypeSymbol(t: Tree, gs: GlobalScope): Type = t match{
		case typ @ Identifier(v) =>
			gs.lookupClass(v) match{
				case None => error("Unknown type", typ.pos); TError
				case Some(p) => typ.setSymbol(p); TObject(p)
			}
		case IntType    => TInt
		case StringType => TString
		case BoolType   => TBool
		case ArrayType  => TArray
		case _          => TUntyped
	}
}
